package data;

import constants.Colors;
import constants.Difficulties;
import constants.Races;
import constants.TileTypes;

import javax.swing.*;
import java.awt.*;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.*;

/**
 * Map class to hold all information about the game and control the game
 *
 * @author Jared Moore
 * @date 10/5/13
 */
public class Map implements Serializable {
	private Difficulties difficulty;
	private final Player[] players;
	private final ArrayList<Colors> availableColors;
	private final Tile[][] map = new Tile[5][9];
	private int currentRound = 1, currentPlayer = 0;
	private Town town;
	private Store store;
	public static final String RANDOM_EVENTS[] = new String[7];

	static {
		RANDOM_EVENTS[0] = "YOU JUST RECEIVED A PACKAGE FROM THE GT ALUMNI CONTAINING 3 FOOD AND 2 ENERGY UNITS.";
		RANDOM_EVENTS[1] = "A WANDERING TECH STUDENT REPAID YOUR HOSPITALITY BY LEAVING TWO BARS OF ORE.";
		RANDOM_EVENTS[2] = "THE MUSEUM BOUGHT YOUR ANTIQUE PERSONAL COMPUTER FOR $8*m.";
		RANDOM_EVENTS[3] = "YOU FOUND A DEAD MOOSE RAT AND SOLD THE HIDE FOR $2*m.";
		RANDOM_EVENTS[4] = "FLYING CAT-BUGS ATE THE ROOF OFF YOUR HOUSE. REPAIRS COST $4*m.";
		RANDOM_EVENTS[5] = "MISCHIEVOUS UGA STUDENTS BROKE INTO YOUR STORAGE SHED AND STOLE HALF YOUR FOOD.";
		RANDOM_EVENTS[6] = "YOUR SPACE GYPSY IN-LAWS MADE A MESS OF THE TOWN. IT COST YOU $6*m TO CLEAN IT UP.";
	}

	/**
	 * Constructor
	 * Sets up the map with the given information
	 *
	 * @param difficulty   The difficulty selected by the user
	 * @param isRandom     True if the user wants a randomly generated map
	 * @param numOfPlayers The number of players in the game
	 *                     <p/>
	 *                     **************************************************
	 *                     NOTE:  For the tile location, the array is [y][x], NOT [x][y]
	 */
	public Map(Difficulties difficulty, boolean isRandom, int numOfPlayers) {
		town = new Town(this);
		this.difficulty = difficulty;
		this.store = new Store(difficulty);
		availableColors = new ArrayList<>(Colors.values().length);
		Collections.addAll(availableColors, Colors.values());

		players = new Player[numOfPlayers];
		currentPlayer = players.length - 1;
		if (isRandom) {
			Random random = new Random();
			for (int i = 0; i < map.length; i++) {
				for (int j = 0; j < map[i].length; j++) {
					int value = random.nextInt(5);
					switch (value) {
						case 4:
							map[i][j] = new Tile(TileTypes.THREE_MOUNTAINS, new Point(j, i));
							break;
						case 3:
							map[i][j] = new Tile(TileTypes.TWO_MOUNTAINS, new Point(j, i));
							break;
						case 2:
							map[i][j] = new Tile(TileTypes.ONE_MOUNTAIN, new Point(j, i));
							break;
						case 1:
							map[i][j] = new Tile(TileTypes.RIVER, new Point(j, i));
							break;
						default:
							map[i][j] = new Tile(TileTypes.PLAIN, new Point(j, i));
					}
				}
			}

			int i = random.nextInt(5), j = random.nextInt(9);
			map[i][j] = new Tile(TileTypes.TOWN, new Point(j, i));

		} else {
			map[0][0] = new Tile(TileTypes.PLAIN, new Point(0, 0));
			map[0][1] = new Tile(TileTypes.PLAIN, new Point(1, 0));
			map[0][2] = new Tile(TileTypes.ONE_MOUNTAIN, new Point(2, 0));
			map[0][3] = new Tile(TileTypes.PLAIN, new Point(3, 0));
			map[0][4] = new Tile(TileTypes.RIVER, new Point(4, 0));
			map[0][5] = new Tile(TileTypes.PLAIN, new Point(5, 0));
			map[0][6] = new Tile(TileTypes.THREE_MOUNTAINS, new Point(6, 0));
			map[0][7] = new Tile(TileTypes.PLAIN, new Point(7, 0));
			map[0][8] = new Tile(TileTypes.PLAIN, new Point(8, 0));

			map[1][0] = new Tile(TileTypes.PLAIN, new Point(0, 1));
			map[1][1] = new Tile(TileTypes.ONE_MOUNTAIN, new Point(1, 1));
			map[1][2] = new Tile(TileTypes.PLAIN, new Point(2, 1));
			map[1][3] = new Tile(TileTypes.PLAIN, new Point(3, 1));
			map[1][4] = new Tile(TileTypes.RIVER, new Point(4, 1));
			map[1][5] = new Tile(TileTypes.PLAIN, new Point(5, 1));
			map[1][6] = new Tile(TileTypes.PLAIN, new Point(6, 1));
			map[1][7] = new Tile(TileTypes.PLAIN, new Point(7, 1));
			map[1][8] = new Tile(TileTypes.ONE_MOUNTAIN, new Point(8, 1));

			map[2][0] = new Tile(TileTypes.THREE_MOUNTAINS, new Point(0, 2));
			map[2][1] = new Tile(TileTypes.PLAIN, new Point(1, 2));
			map[2][2] = new Tile(TileTypes.PLAIN, new Point(2, 2));
			map[2][3] = new Tile(TileTypes.PLAIN, new Point(3, 2));
			map[2][4] = new Tile(TileTypes.TOWN, new Point(4, 2));
			map[2][5] = new Tile(TileTypes.PLAIN, new Point(5, 2));
			map[2][6] = new Tile(TileTypes.PLAIN, new Point(6, 2));
			map[2][7] = new Tile(TileTypes.PLAIN, new Point(7, 2));
			map[2][8] = new Tile(TileTypes.ONE_MOUNTAIN, new Point(8, 2));

			map[3][0] = new Tile(TileTypes.PLAIN, new Point(0, 3));
			map[3][1] = new Tile(TileTypes.TWO_MOUNTAINS, new Point(1, 3));
			map[3][2] = new Tile(TileTypes.PLAIN, new Point(2, 3));
			map[3][3] = new Tile(TileTypes.PLAIN, new Point(3, 3));
			map[3][4] = new Tile(TileTypes.RIVER, new Point(4, 3));
			map[3][5] = new Tile(TileTypes.PLAIN, new Point(5, 3));
			map[3][6] = new Tile(TileTypes.TWO_MOUNTAINS, new Point(6, 3));
			map[3][7] = new Tile(TileTypes.PLAIN, new Point(7, 3));
			map[3][8] = new Tile(TileTypes.PLAIN, new Point(8, 3));

			map[4][0] = new Tile(TileTypes.PLAIN, new Point(0, 4));
			map[4][1] = new Tile(TileTypes.PLAIN, new Point(1, 4));
			map[4][2] = new Tile(TileTypes.TWO_MOUNTAINS, new Point(2, 4));
			map[4][3] = new Tile(TileTypes.PLAIN, new Point(3, 4));
			map[4][4] = new Tile(TileTypes.RIVER, new Point(4, 4));
			map[4][5] = new Tile(TileTypes.PLAIN, new Point(5, 4));
			map[4][6] = new Tile(TileTypes.PLAIN, new Point(6, 4));
			map[4][7] = new Tile(TileTypes.PLAIN, new Point(7, 4));
			map[4][8] = new Tile(TileTypes.TWO_MOUNTAINS, new Point(8, 4));
		}
	}

	/**
	 * Builds a player based on the given information
	 *
	 * @param name  The name of the player
	 * @param race  The race of the player
	 * @param color The color of the player
	 * @return Whether or not the player was set
	 *         <p/>
	 *         precondition:  All of the players have not been set up yet
	 */
	public boolean buildPlayer(String name, Races race, Colors color) {
		for (int i = 0; i < players.length; i++) {
			if (players[i] == null) {
				int[] resources = new int[4];
				switch (difficulty) {
					case BEGINNER:
						resources[0] = 8;
						resources[1] = 4;
						break;
					default:
						resources[0] = 4;
						resources[1] = 2;
						break;
				}
				players[i] = new Player(name, race, color, resources);
				availableColors.remove(color);
				if (i == 0) {
					map[0][0].setOnTile(players[0]);
				}
				if (i + 1 < players.length) {
					return true;
				} else {
					break;
				}
			}
		}
		return false;
	}

	/**
	 * Gets the available colors so two players cannot have the same color
	 *
	 * @return An ArrayList with the colors that have not been claimed in it
	 */
	public ArrayList<Colors> getAvailableColors() {
		return availableColors;
	}

	/**
	 * Getter for the map
	 *
	 * @return The 2-d array of tiles that represents the map
	 */
	public Tile[][] getMap() {
		return map;
	}

	/**
	 * Gets the current player
	 *
	 * @return The player whose turn it is
	 */
	public Player getCurrentPlayer() {
		return players[currentPlayer];
	}

	/**
	 * Gets the turn length for a player
	 *
	 * @param myTurn The player in question
	 * @return The length of that player's turn
	 */
	public int getTurnLength(Player myTurn) {
		if (myTurn.getFood() > 0) {
			return 50000;
		} else if (myTurn.getFood() == 0) {
			return 30000;
		} else {
			// if less than 0, set to 0
			myTurn.addFood(myTurn.getFood() * -1);
			return 5000;
		}
	}

	/**
	 * Moves to the next turn
	 */
	public void nextTurn() {
		JOptionPane.showMessageDialog(null, String.format("%s's turn is over, on to the next",
				getCurrentPlayer().getName()));
		store.recalculatePrices();
		//Sorts players in ascending order
		if (currentPlayer == 0) {
			currentPlayer = players.length - 1;
			currentRound++;
			decrementFood();
			Arrays.sort(players);
			HashMap<Player, Integer[]> productionMap = calculateProduction();
			for (Player i : players) {
				if (productionMap.containsKey(i)) {
					i.addFood(productionMap.get(i)[0]);
					i.addEnergy(productionMap.get(i)[1]);
					i.addOre(productionMap.get(i)[2]);
				}
			}

		} else {
			currentPlayer--;
		}
		boolean playerHasLocation = false;
		findPlayer:
		for (Tile[] i : map) {
			for (Tile j : i) {
				if (j.getOnTile() == players[currentPlayer]) {
					playerHasLocation = true;
					break findPlayer;
				}
			}
		}
		if (!playerHasLocation) {
			// give the player a location if they don't have one
			addPlayer:
			for (Tile[] i : map) {
				for (Tile j : i) {
					if (j.getOnTile() == null && !j.isTown()) {
						j.setOnTile(players[currentPlayer]);
						break addPlayer;
					}
				}
			}
		}
		if (currentRound > 1) {
			randomEvent();
		}
		save();
	}

	/**
	 * Gets the list of players
	 *
	 * @return The players
	 */
	public Player[] getPlayers() {
		return players;
	}

	/**
	 * Gets the town
	 *
	 * @return The town associated with this map
	 */
	public Town getTown() {
		return town;
	}

	public Store getStore() {
		return store;
	}

	/**
	 * Signifies that the player has entered the pub and wants to gamble
	 * Increases the player's money by the appropriate amount
	 *
	 * @param timeLeft The amount of time left in the player's turn
	 */
	public void gamble(int timeLeft) {
		int roundBonus = (currentRound / 4) * 50 + 50;
		int timeBonus = (timeLeft - 1) / 12 * 50 + 50;

		int reward = (int) (roundBonus * Math.random() * timeBonus);
		reward = (reward < 250 ? reward : 250);
		players[currentPlayer].addMoney(reward);
		JOptionPane.showMessageDialog(null, String.format("%s won %d money by gambling",
				players[currentPlayer].getName(), reward));
		nextTurn();
	}

	/**
	 * Removes the variable from the string and replaces it with an actual value
	 *
	 * @param event The index of RANDOM_EVENTS to use
	 * @param m     The value for m
	 * @return The string with #*m replaced with a number
	 */
	public String removeM(int event, int m) {

		String mString = RANDOM_EVENTS[event];
		int value = Integer.parseInt(mString.replaceAll("[\\D]", ""));
		return mString.replaceAll("[0-9]\\*m", String.format("%d", m * value));
	}

	/**
	 * Places a MULE on a tile
	 *
	 * @param mule The MULE to be set on a tile
	 */
	public void setMuleOnTile(MULE mule) {
		if (mule == null) {
			return;
		}
		if (players[currentPlayer].hasFreeMule()) {
			int x = 0;
			int y = 0;
			findPlayer:
			for (Tile[] i : map) {
				for (Tile j : i) {
					if (j.getOnTile() == players[currentPlayer]) {
						x = j.getTileLocation().x;
						y = j.getTileLocation().y;
						break findPlayer;
					}
				}
			}
			if (map[y][x].getOwner() == players[currentPlayer]) {
				if (map[y][x].getMule() == null) {
					map[y][x].placeMule(mule);
					players[currentPlayer].setFreeMule(null);
				} else {
					//player lost his or her mule because there was already a mule there
					players[currentPlayer].setFreeMule(null);
				}
			} else {
				//player lost his or her mule because that was not the current player's tile
				players[currentPlayer].setFreeMule(null);
			}
		}
	}

	/**
	 * Saves the game to the file
	 */
	public boolean save() {
		try {
			ObjectOutputStream output = new ObjectOutputStream(new FileOutputStream("game.ser"));
			output.writeObject(this);
			output.close();
		} catch (IOException e) {
			e.printStackTrace();
			System.err.print("Error saving file, make sure you have write access on the active directory");
			System.exit(1);
			return false;
		}
		return true;
	}

	/**
	 * Decrements all players food based on the amount they need for each round
	 */
	private void decrementFood() {
		int amount = (currentRound - 1) / 4 + 3;
		for (Player i : players) {
			i.decrementFood(amount);
		}
	}

	/**
	 * Generates a HashMap for the players' gains due to production
	 *
	 * @return A HashMap with the Player as the key and their production as the value
	 */
	private HashMap<Player, Integer[]> calculateProduction() {

		HashMap<Player, Integer[]> productionMap = new HashMap<>();
		for (Player i : players) {
			for (Tile j : i.getProperties()) {
				int[] prod = j.calculateProduction();
				Integer production[] = new Integer[4];
				production[0] = prod[0];
				production[1] = prod[1];
				production[2] = prod[2];
				production[3] = prod[3];
				if (productionMap.containsKey(i)) {
					productionMap.put(i, addArrays(productionMap.get(i), production));
				} else {
					productionMap.put(i, production);
				}
			}
		}

		return productionMap;
	}

	/**
	 * Adds two Integer arrays
	 *
	 * @param array1 The first array
	 * @param array2 The second array
	 * @return The sum of the two arrays
	 */
	private Integer[] addArrays(Integer[] array1, Integer[] array2) {

		if (array1.length != array2.length) {
			System.err.print("Arrays are incompatible, what are you doing?");
			System.exit(1);
		}
		for (int i = 0; i < array1.length; i++) {
			array1[i] += array2[i];
		}
		return array1;
	}

	/**
	 * Generates a random event 27% of the time for the current player
	 */
	private void randomEvent() {
		int m = (currentRound % 4) * 25 + 25;
		Random random = new Random();

		if (random.nextInt(100) < 27) {
			int event = random.nextInt((currentPlayer == players.length - 1) ? 4 : 7);
			switch (event) {
				case 6:
					String output6 = removeM(6, m);
					int amount6 = Integer.parseInt(output6.replaceAll("[\\D]", ""));
					if (players[currentPlayer].hasMoney(amount6)) {
						players[currentPlayer].addMoney(-1 * amount6);
					} else {
						players[currentPlayer].addMoney(-1 * players[currentPlayer].getMoney());
					}
					JOptionPane.showMessageDialog(null, output6);
					break;
				case 5:
					players[currentPlayer].addFood(-1 * players[currentPlayer].getFood() / 2);
					JOptionPane.showMessageDialog(null, RANDOM_EVENTS[5]);
					break;
				case 4:
					String output4 = removeM(4, m);
					int amount4 = Integer.parseInt(output4.replaceAll("[\\D]", ""));
					if (players[currentPlayer].hasMoney(amount4)) {
						players[currentPlayer].addMoney(-1 * amount4);
					} else {
						players[currentPlayer].addMoney(-1 * players[currentPlayer].getMoney());
					}
					JOptionPane.showMessageDialog(null, output4);
					break;
				case 3:
					String output3 = removeM(6, m);
					int amount3 = Integer.parseInt(output3.replaceAll("[\\D]", ""));
					players[currentPlayer].addMoney(amount3);
					JOptionPane.showMessageDialog(null, output3);
					break;
				case 2:
					String output2 = removeM(6, m);
					int amount2 = Integer.parseInt(output2.replaceAll("[\\D]", ""));
					players[currentPlayer].addMoney(amount2);
					JOptionPane.showMessageDialog(null, output2);
					break;
				case 1:
					players[currentPlayer].addOre(2);
					JOptionPane.showMessageDialog(null, RANDOM_EVENTS[1]);
					break;
				case 0:
					players[currentPlayer].addFood(3);
					players[currentPlayer].addEnergy(2);
					JOptionPane.showMessageDialog(null, RANDOM_EVENTS[0]);
					break;
			}
		}
	}
}